﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Shapes;
using System.Windows.Media;
using System.Windows;
using System.Windows.Input;
using System.ComponentModel;
using System.Reflection;
using System.Xml.Linq;
using System.Globalization;

namespace remes.XDraw
{
   public enum XDrawingModes
   {
      Select,
      Edit,
      NewLine,
      NewRect,
      NewEllipse,
      NewPath
   }

   public enum XDrawingExportFormat
   {
      Canvas,
      DrawingImage
   }

   public class XDrawing : DependencyObject
   {
      public static readonly DependencyProperty SelectedShapeProperty = DependencyProperty.Register(
         "SelectedShape", typeof(XDrawingShape), typeof(XDrawing), new PropertyMetadata(null));
      public static readonly DependencyProperty SelectedControlPointProperty = DependencyProperty.Register(
         "SelectedControlPoint", typeof(ControlPoint), typeof(XDrawing), new PropertyMetadata(null));
      public static readonly DependencyProperty ModeProperty = DependencyProperty.Register(
         "Mode", typeof(XDrawingModes), typeof(XDrawing), new PropertyMetadata(XDrawingModes.Select, ModeChanged));

      public XDrawing(Canvas canvas)
      {
         Canvas = canvas;

         Canvas.PreviewMouseDown += Canvas_PreviewMouseDown;
         Canvas.PreviewMouseUp += Canvas_PreviewMouseUp;
         Canvas.PreviewMouseMove += Canvas_PreviewMouseMove;

         foreach (var child in Canvas.Children)
         {
            var p = child as Path;
            if (p != null)
            {
               if (p.Name == "controlPoints")
               {
                  m_ControlPointPath = p;

                  m_ControlPointGroup = p.Data as GeometryGroup;
               }
               else if (p.Name == "selectedControlPoints")
               {
                  m_SelectedControlPointPath = p;

                  m_SelectedControlPointGroup = p.Data as GeometryGroup;
               }
               else if (p.Name == "controlLines")
               {
                  m_ControlLinePath = p;

                  m_ControlLineGroup = p.Data as GeometryGroup;
               }
               else
               {
                  if (p.Data is RectangleGeometry)
                  {
                     m_Shapes.Add(new XDrawingRectangle(this, p));
                  }
                  else if (p.Data is PathGeometry)
                  {
                     m_Shapes.Add(new XDrawingPath(this, p));
                  }
               }
            }
         }
      }

      public XElement Export(XDrawingExportFormat format)
      {
         XElement xDrawing;
         XElement xParent;
         switch (format)
         {
            case XDrawingExportFormat.Canvas:
               xDrawing = new XElement("Canvas");
               xParent = xDrawing;
               break;

            case XDrawingExportFormat.DrawingImage:
               xParent = new XElement("DrawingGroup");

               xDrawing = new XElement("DrawingImage",
                  new XElement("DrawingImage.Drawing",
                     xParent));
               break;

            default:
               throw new ArgumentException("format not supported");
         }
         foreach (var shape in m_Shapes)
         {
            shape.Export(xParent, format);
         }
         return xDrawing;
      }

      /// <summary>
      /// Imports a drawing from xml.
      /// </summary>
      /// <param name="xElement">Xml element too start searching</param>
      /// <returns>true if all xml element could be loaded, false if unknown (skiped) elements have been found</returns>
      /// <remarks>
      /// Searches for the 1st DrawingImage, if none is found look for Canvas
      /// </remarks>
      public bool Import(XElement xRoot)
      {
         return Import(xRoot, true);
      }

      /// <summary>
      /// Imports a drawing from xml.
      /// </summary>
      /// <param name="xElement">Xml element too start searching</param>
      /// <param name="clearDrawing">remove all shapes before import</param>
      /// <returns>true if all xml element could be loaded, false if unknown (skiped) elements have been found</returns>
      /// <remarks>
      /// Searches for the 1st DrawingImage, if none is found look for Canvas
      /// </remarks>
      public bool Import(XElement xElement, bool clearDrawing)
      {
         if (clearDrawing)
         {
            ClearDrawing();
         }

         // look for DrawingImage
         var xRoot = (from e in xElement.DescendantsAndSelf("DrawingImage")
                      select e).FirstOrDefault();
         if (xRoot != null)
         {
            // we need the 1st DrawingGroup
            xRoot = (from e in xElement.Descendants("DrawingGroup")
                     select e).FirstOrDefault();

            if (xRoot != null)
            {
               foreach (var xGD in xRoot.Elements("GeometryDrawing"))
               {
                  XDrawingShape shape = XDrawingShape.LoadFromGeometryDrawing(this, xGD);
                  if (shape != null)
                  {
                     m_Shapes.Add(shape);
                     Canvas.Children.Insert(Canvas.Children.IndexOf(m_ControlPointPath), shape.Path);
                  }
               }
            }
         }
         else
         {
            // DrawingImage not found, look for Canvas
            xRoot = (from e in xElement.DescendantsAndSelf("Canvas")
                     select e).FirstOrDefault();

            if (xRoot != null)
            {
               foreach (var xPath in xRoot.Elements("Path"))
               {
                  XDrawingShape shape = XDrawingShape.LoadFromPath(this, xPath);
                  if(shape != null)
                  {
                     m_Shapes.Add(shape);
                     Canvas.Children.Insert(Canvas.Children.IndexOf(m_ControlPointPath), shape.Path);
                  }
               }
            }
         }

         return true;
      }

      public void RemoveShape(XDrawingShape shape)
      {
         if (SelectedShape == shape)
         {
            SelectedControlPoint = null;
            SelectedShape = null;
         }
         shape.Dispose();
         m_Shapes.Remove(shape);
      }

      public void ClearDrawing()
      {
         SelectedControlPoint = null;
         SelectedShape = null;
         foreach (var shape in Shapes)
         {
            shape.Dispose();
         }
         m_Shapes.Clear();
      }

      public XDrawingModes Mode
      {
         get { return (XDrawingModes)GetValue(ModeProperty); }
         set { SetValue(ModeProperty, value); }
      }

      private static void ModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
      {
         var drawing = d as XDrawing;
         if (drawing != null && drawing.SelectedShape != null && (XDrawingModes)e.NewValue != (XDrawingModes)e.OldValue)
         {
            drawing.ClearControlPoints();
            drawing.SelectedShape.CreateControlPoints(((XDrawingModes)e.NewValue) == XDrawingModes.Edit);
         }
      }

      private HitTestInfo? m_DragInfo = null;

      void Canvas_PreviewMouseDown(object sender, MouseButtonEventArgs e)
      {
         var pt = e.GetPosition(Canvas);

         switch (Mode)
         {
            case XDrawingModes.Select:
            case XDrawingModes.Edit:
               m_DragInfo = HitTest(pt);
               if (!m_DragInfo.HasValue)
               {
                  SelectedShape = null;
               }
               else
               {
                  if (m_DragInfo.Value.Shape != SelectedShape)
                  {
                     SelectedShape = m_DragInfo.Value.Shape;
                  }
                  if (m_DragInfo.Value.ControlPoint != SelectedControlPoint)
                  {
                     if (m_DragInfo.Value.ControlPoint == null || m_DragInfo.Value.ControlPoint.IsSelectable)
                     {
                        SelectedControlPoint = m_DragInfo.Value.ControlPoint;
                     }
                  }
                  m_DragInfo.Value.DragObject.StartDrag(pt);
               }
               break;

            case XDrawingModes.NewLine:
            case XDrawingModes.NewRect:
            case XDrawingModes.NewEllipse:
            case XDrawingModes.NewPath:
               SelectedShape = null;
               XDrawingModes newMode = Mode;
               switch(Mode)
               {
                  case XDrawingModes.NewLine:
                     m_DragInfo = XDrawingLine.CreateNewByDrag(this, pt, out newMode);
                     break;

                  case XDrawingModes.NewRect:
                     m_DragInfo = XDrawingRectangle.CreateNewByDrag(this, pt, out newMode);
                     break;

                  case XDrawingModes.NewEllipse:
                     m_DragInfo = XDrawingEllipse.CreateNewByDrag(this, pt, out newMode);
                     break;

                  case XDrawingModes.NewPath:
                     m_DragInfo = XDrawingPath.CreateNewByDrag(this, pt, out newMode);
                     break;
               }
               if (m_DragInfo.HasValue)
               {
                  Mode = newMode;
                  SetValue(SelectedShapeProperty, m_DragInfo.Value.Shape);
                  m_Shapes.Add(m_DragInfo.Value.Shape);
                  Canvas.Children.Insert(Canvas.Children.IndexOf(m_ControlPointPath), m_DragInfo.Value.Path);
                  m_DragInfo.Value.DragObject.StartDrag(pt);
               }
               break;
         }
         Canvas.CaptureMouse();
      }

      void Canvas_PreviewMouseUp(object sender, MouseButtonEventArgs e)
      {
         var pt = e.GetPosition(Canvas);
         if (m_DragInfo.HasValue)
         {
            m_DragInfo.Value.DragObject.EndDrag(pt);
            m_DragInfo = null;
         }
         Canvas.ReleaseMouseCapture();
      }

      void Canvas_PreviewMouseMove(object sender, MouseEventArgs e)
      {
         var pt = e.GetPosition(Canvas);
         if (m_DragInfo.HasValue)
         {
            m_DragInfo.Value.DragObject.DragObject(pt);
         }
      }

      public Canvas Canvas { get; private set; }

      private Path m_ControlPointPath;
      private GeometryGroup m_ControlPointGroup;
      private Path m_SelectedControlPointPath;
      private GeometryGroup m_SelectedControlPointGroup;
      private Path m_ControlLinePath;
      private GeometryGroup m_ControlLineGroup;

      internal GeometryGroup ControlPointGroup
      {
         get { return m_ControlPointGroup; }
      }

      internal GeometryGroup SelectedControlPointGroup
      {
         get { return m_SelectedControlPointGroup; }
      }

      internal GeometryGroup ControlLineGroup
      {
         get { return m_ControlLineGroup; }
      }

      public XDrawingShape SelectedShape
      {
         get { return (XDrawingShape)GetValue(SelectedShapeProperty); }
         set
         {
            if (SelectedShape != value)
            {
               ClearControlPoints();
               SetValue(SelectedShapeProperty, value);
               if (value != null)
               {
                  value.CreateControlPoints(Mode == XDrawingModes.Edit);
               }
            }
         }
      }

      public ControlPoint SelectedControlPoint
      {
         get { return (ControlPoint)GetValue(SelectedControlPointProperty); }
         set
         {
            if (SelectedControlPoint != value)
            {
               if (SelectedControlPoint != null)
               {
                  SelectedControlPoint.IsSelected = false;
               }
               if (value != null)
               {
                  value.IsSelected = true;
               }
               SetValue(SelectedControlPointProperty, value);
            }
         }
      }

      private List<ControlPoint> m_ControlPoints = new List<ControlPoint>();
      private List<XDrawingShape> m_Shapes = new List<XDrawingShape>();

      public IEnumerable<XDrawingShape> Shapes
      {
         get { return m_Shapes; }
      }

      public IEnumerable<XDrawingShape> InverseShapes
      {
         get
         {
            for (int n = m_Shapes.Count - 1; n >= 0; --n)
            {
               yield return m_Shapes[n];
            }
         }
      }

      internal void ClearControlPoints()
      {
         for (int n = m_ControlPoints.Count - 1; n >= 0; --n)
         {
            m_ControlPoints[n].Dispose();
         }
         m_ControlPoints.Clear();
      }

      internal void AddControlPoint(ControlPoint cp)
      {
         m_ControlPoints.Add(cp);
      }

      private bool IsControlPath(Path path)
      {
         return path != m_ControlPointPath && path != m_SelectedControlPointPath && path != m_ControlLinePath;
      }

      public IEnumerable<Path> GetPathObjects(bool inverse)
      {
         if (inverse)
         {
            for (int n = Canvas.Children.Count - 1; n >= 0; --n)
            {
               if (Canvas.Children[n] is Path && !IsControlPath(Canvas.Children[n] as Path))
               {
                  yield return Canvas.Children[n] as Path;
               }
            }
         }
         else
         {
            foreach (var c in Canvas.Children)
            {
               if (c is Path && c != m_ControlPoints)
               {
                  yield return c as Path;
               }
            }
         }
      }

      private HitTestInfo? HitTest(Point pt)
      {
         HitTestInfo? hti = null;
         foreach (var cp in m_ControlPoints)
         {
            hti = cp.HitTest(pt);
            if (hti.HasValue)
            {
               return hti;
            }
         }

         if (SelectedShape != null)
         {
            hti = SelectedShape.HitTest(pt);
            if (hti.HasValue)
            {
               return hti;
            }
         }

         foreach (var p in InverseShapes)
         {
            hti = p.HitTest(pt);
            if (hti.HasValue)
            {
               return hti;
            }
         }
         return null;
      }
   }

   public interface IDragableObject
   {
      HitTestInfo? HitTest(Point pt);
      void StartDrag(Point pt);
      void DragObject(Point pt);
      void EndDrag(Point pt);
      bool IsDraged { get; }
   }

   public struct HitTestInfo
   {
      public Path Path;
      public XDrawingShape Shape;
      public ControlPoint ControlPoint;
      public Vector Offset;

      public IDragableObject DragObject
      {
         get { return (ControlPoint as IDragableObject) ?? (Shape as IDragableObject); }
      }
   }
}
